This example is inspired from the plot-gtk-ui package. Our goal will be to create an interface similar to the screenshot below.
In [1]:
{-# LANGUAGE OverloadedStrings #-}
import IHaskell.Display.Widgets
First, we create a common structure that will hold all the information required to create a plot. This has to be done first so that we can hook widget events to modify it. The plotting logic is implemented next for the same reason.
In [2]:
import Data.IORef
import Data.Monoid (mempty)
import Data.Text (Text)
import qualified Data.Map as M
data PlotInfo = PlotInfo {
plotTitle :: String,
plotTitleSize :: Double,
xLabel :: String,
xLabelSize :: Double,
yLabel :: String,
yLabelSize :: Double,
showXGrid :: Bool,
showYGrid :: Bool,
xRange :: (Double, Double),
yRange :: (Double, Double),
sampling :: Double,
functions :: M.Map String (Double -> Double)
}
defaultPlotInfo = PlotInfo {
plotTitle = mempty,
plotTitleSize = 10,
xLabel = mempty,
xLabelSize = 10,
yLabel = mempty,
yLabelSize = 10,
showXGrid = True,
showYGrid = True,
xRange = (-5, 5),
yRange = (-5, 5),
sampling = 50,
functions = mempty
}
Now, we implement the plotting logic. We also create an ImageWidget here, which will be used to display the plot.
In [3]:
import Data.IORef
import Graphics.Rendering.Chart.Easy hiding (tan)
import Graphics.Rendering.Chart.Backend.Cairo
import qualified Data.ByteString as B
import IHaskell.Display (base64)
import Control.Applicative ((<$>))
tempImgWidget <- mkImageWidget
setField tempImgWidget Width 400
setField tempImgWidget Height 400
plotState <- newIORef defaultPlotInfo
-- Update and redraw.
update :: (PlotInfo -> IO PlotInfo) -> IO ()
update modifier = readIORef plotState >>= modifier >>= writeIORef plotState >> redraw
redraw :: IO ()
redraw = readIORef plotState >>= mkPlot >>= setField tempImgWidget B64Value . base64
mkDset :: PlotInfo -> [(String, [(Double, Double)])]
mkDset info = let funcs = M.toList $ functions info
(xLow, xHigh) = xRange info
period = 1 / sampling info
xs = [xLow, xLow + period .. xHigh]
in map (\(s, f) -> (s, zip xs $ map f xs)) funcs
axisSetter :: Bool -> Bool -> AxisData t -> AxisData t
axisSetter axis grid ad =
ad { _axis_grid = if grid then _axis_grid ad else []
, _axis_visibility = if axis
then AxisVisibility True True True
else AxisVisibility False False False
}
mkPlot :: PlotInfo -> IO B.ByteString
mkPlot info = do
let dset = mkDset info
opts = def { _fo_size = (400, 400) }
toFile opts ".chart" $ do
layout_title .= plotTitle info
layout_title_style . font_size .= plotTitleSize info
layout_x_axis . laxis_title .= xLabel info
layout_x_axis . laxis_title_style . font_size .= xLabelSize info
layout_x_axis . laxis_generate .= scaledAxis def (xRange info)
layout_x_axis . laxis_override .= if showXGrid info then id else axisGridHide
layout_y_axis . laxis_title .= yLabel info
layout_y_axis . laxis_title_style . font_size .= yLabelSize info
layout_y_axis . laxis_generate .= scaledAxis def (yRange info)
layout_y_axis . laxis_override .= if showYGrid info then id else axisGridHide
mapM_ (\(s, ps) -> plot (line s [ps])) dset
B.readFile ".chart"
All that's left now is to create an interface and hook widget events accordingly.
The first required element is a box, to create a vertical division between the plotting region and the input widgets.
In [4]:
divBox <- mkFlexBox
setField divBox Orientation HorizontalOrientation
-- Two parts: A FlexBox for the left part (plot + sliders) and an Accordion for the input elements.
plBox <- mkFlexBox
tlBox <- mkAccordion
-- Add the widgets to the main dividing box.
setField divBox Children [ChildWidget plBox, ChildWidget tlBox]
-- Make the orientation Vertical
setField plBox Orientation VerticalOrientation
Now we fill in the plotting area with:
FlexBox to hold the sliders.ImageWidget to hold the plot.
In [5]:
slBox <- mkFlexBox
-- Reusing the image widget created before
let plImg = tempImgWidget
-- Sliders need to be laid out vertically.
setField slBox Orientation VerticalOrientation
-- Add widgets to the plotting region.
setField plBox Children [ChildWidget slBox, ChildWidget plImg]
Now, we fill the other half with the following:
FlexBox widgets (title, sub-title, x-label, y-label), containing a TextWidget for title and a BoundedFloatText for the font size.FlexBox with two selection widgets for toggling visibility for different elements. We'll go with ToggleButton just for fun.FlexBox, with FloatText widgets for deciding the plot range.
In [6]:
-- The four FlexBox widgets.
import Control.Monad (replicateM, forM_)
import Data.List (zip4)
import Text.Printf (printf)
import Data.Text (unpack, pack)
-- pl : plotTitle
-- x : xLabel
-- y : yLabel
boxes <- replicateM 3 mkFlexBox
texts@[plTxt,xTxt,yTxt] <- replicateM 3 mkTextWidget
inpts@[plInp,xInp,yInp] <- replicateM 3 mkBoundedFloatText
-- Adding event handlers for text widgets. This is a clumsy way to emulate first-class record fields.
let setHandler widget field = setField widget ChangeHandler $ update $ \info -> do
newStr <- getField widget StringValue
return $ field info newStr
in do
setHandler plTxt $ \struct val -> struct { plotTitle = unpack val }
setHandler xTxt $ \struct val -> struct { xLabel = unpack val }
setHandler yTxt $ \struct val -> struct { yLabel = unpack val }
-- Adding events for the numeric input widgets.
let setHandler widget field = setField widget ChangeHandler $ update $ \info -> do
newNum <- getField widget FloatValue
return $ field info newNum
in do
setHandler plInp $ \struct val -> struct { plotTitleSize = val }
setHandler xInp $ \struct val -> struct { xLabelSize = val }
setHandler yInp $ \struct val -> struct { yLabelSize = val }
let boxInfo = zip4 boxes texts inpts ["plot title", "X-Label", "Y-Label"]
forM_ boxInfo $ \(box,text,input,placeholder) -> do
setField box Orientation HorizontalOrientation
setField box Children [ChildWidget text, ChildWidget input]
setField text Placeholder $ pack $ printf "Enter %s here ..." placeholder
setField input MinFloat 1
setField input MaxFloat 72
setField input FloatValue 10
In [7]:
-- A FlexBox with ToggleButtons
buttonBox <- mkFlexBox
setField buttonBox Orientation HorizontalOrientation
tButtons@[xGrid,yGrid] <- replicateM 2 mkToggleButton
let tgButtonInfo = zip tButtons ["X-Grid", "Y-Grid"]
let setHandler widget fieldSetter = setField widget ChangeHandler $ update $ \info -> do
newStr <- getField widget BoolValue
return $ fieldSetter info newStr
in do
setHandler xGrid $ \struct val -> struct { showXGrid = val }
setHandler yGrid $ \struct val -> struct { showYGrid = val }
forM_ tgButtonInfo $ \(widget, description) -> do
setField widget Description description
setField widget BoolValue True
setField buttonBox Children (map ChildWidget tButtons)
In [8]:
import Control.Arrow (first, second)
-- Finally, the ranges
rangeBoxes <- replicateM 2 mkFlexBox
fTxts@[xLow,xHigh,yLow,yHigh] <- replicateM 4 mkFloatText
let rangeInfo = zip rangeBoxes [(xLow,xHigh), (yLow, yHigh)]
forM_ rangeInfo $ \(box, (lowTxt, highTxt)) -> do
setField box Orientation HorizontalOrientation
setField box Children (map ChildWidget [lowTxt, highTxt])
let setHandler widget modifier = setField widget ChangeHandler $ update $ \info -> do
val <- getField widget FloatValue
return $ modifier val info
in do
setHandler xLow $ \v p -> p { xRange = first (const v) (xRange p) }
setHandler xHigh $ \v p -> p { xRange = second (const v) (xRange p) }
setHandler yLow $ \v p -> p { yRange = first (const v) (yRange p) }
setHandler yHigh $ \v p -> p { yRange = second (const v) (yRange p) }
Now, to finally add these widgets to the right part of the window.
In [9]:
setField tlBox Children $ map ChildWidget $ boxes ++ [buttonBox] ++ rangeBoxes
We also need to give a title to each page in the Accordion widget.
In [10]:
setField tlBox Titles ["Plot title", "X-Label", "Y-Label", "Grid", "X-range", "Y-range"]
Then we sync the initial values from the plotData to the widgets.
In [11]:
let syncVal widget value fieldGetter = readIORef plotState >>= setField widget value . fieldGetter
in do
syncVal plTxt StringValue (pack . plotTitle)
syncVal plInp FloatValue plotTitleSize
syncVal xTxt StringValue (pack . xLabel)
syncVal xInp FloatValue xLabelSize
syncVal yTxt StringValue (pack . yLabel)
syncVal yInp FloatValue yLabelSize
syncVal xGrid BoolValue showXGrid
syncVal yGrid BoolValue showYGrid
syncVal xLow FloatValue (fst . xRange)
syncVal xHigh FloatValue (snd . xRange)
syncVal yLow FloatValue (fst . yRange)
syncVal yHigh FloatValue (snd . yRange)
Now that everything is set, we also need to provide a way for the user to add or remove plots from the interface.
In [12]:
addFunction :: String -> (Double -> Double) -> IO ()
addFunction name func = update $ \p -> return p { functions = M.insert name func $ functions p }
removeFunction :: String -> IO ()
removeFunction name = update $ \p -> return p { functions = M.delete name $ functions p }
And now we display the complete interface, ready to use.
In [13]:
-- Spurious update to display empty plot instead of empty image initially
update return
divBox
Now, we can use addFunction and removeFunction to add and remove functions respectively.
In [14]:
addFunction "sin" sin
In [15]:
addFunction "cos" cos
In [16]:
addFunction "x^2" (\x -> x * x)